package org.baojie.raft.store;

import org.baojie.raft.node.NodeName;
import org.baojie.raft.node.type.Role;
import org.baojie.raft.serialization.RaftSerialize;
import org.baojie.raft.serialization.Serialize;
import org.baojie.raft.util.current.RaftUnsafe;
import sun.misc.Unsafe;

import javax.print.attribute.standard.MediaSize;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class RaftNodeStore implements NodeStore {

    static {
        Unsafe unsafe = RaftUnsafe.getUnsafe();
        if (null == unsafe) {
            throw new Error("raft unsafe null ...");
        } else {
            pageSize = unsafe.pageSize();
        }
    }

    public static final int pageSize;

    public static final String TAIL = ".cid";
    private static final int OFFSET_TERM = 0;
    private static final int OFFSET_VOTED_FOR = 4;

    private final Serialize serialize = RaftSerialize.getInstance();

    private final ReentrantReadWriteLock mainLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = mainLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = mainLock.writeLock();

    private final Thread hook = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                doClose();
            } catch (Exception e) {

            }
        }
    }, "raft node store clean mapped hook");

    private final File dir;
    private final File file;
    private final NodeName local;

    private volatile boolean open = false;
    private volatile int term = 0;
    private volatile NodeName votedFor = null;

    private RandomAccessFile randomFile;

    public RaftNodeStore(File file, NodeName local) {
        if (!file.isDirectory()) {
            throw new IllegalArgumentException("file must be an directory ...");
        }
        this.local = local;
        this.dir = makeDir(file);
        this.file = makeFile(dir);
    }

    private File makeDir(File file) {
        String path = file.getAbsolutePath();
        StringBuilder builder = new StringBuilder(256);
        if (path.endsWith(File.separator)) {
            builder.append(path);
            builder.append(local.getName());
            builder.append(File.separator);
        } else {
            builder.append(path);
            builder.append(File.separator);
            builder.append(local.getName());
            builder.append(File.separator);
        }
        String real = builder.toString();
        return new File(real);
    }

    private File makeFile(File file) {
        String path = file.getAbsolutePath();
        StringBuilder builder = new StringBuilder(256);
        if (path.endsWith(File.separator)) {
            builder.append(path);
            builder.append(local.getName());
            builder.append(TAIL);
        } else {
            builder.append(path);
            builder.append(File.separator);
            builder.append(local.getName());
            builder.append(TAIL);
        }
        String real = builder.toString();
        return new File(real);
    }

    @Override
    public void open() throws IOException {
        final ReentrantReadWriteLock.WriteLock write = writeLock;
        write.lock();
        try {
            if (!open) {
                boolean newer = needCreate();
                openMapper(newer);
                doLoad();
                open = true;
                Runtime.getRuntime().addShutdownHook(hook);
            }
        } finally {
            write.unlock();
        }
    }

    private boolean needCreate() throws IOException {
        if (!dir.exists()) {
            boolean suc = dir.mkdirs();
            if (!suc) {
                throw new IOException();
            }
            suc = file.createNewFile();
            if (!suc) {
                throw new IOException();
            }
            long now = System.currentTimeMillis();
            suc = file.setLastModified(now);
            if (!suc) {
                throw new IOException();
            }
            return true;
        } else {
            if (!file.exists()) {
                boolean suc = file.createNewFile();
                if (!suc) {
                    throw new IOException();
                }
                long now = System.currentTimeMillis();
                suc = file.setLastModified(now);
                if (!suc) {
                    throw new IOException();
                }
                return true;
            } else {
                return false;
            }
        }
    }

    private void openMapper(boolean newer) throws IOException {
        randomFile = new RandomAccessFile(file, "rw");
        if (newer) {
            randomFile.setLength(0);
        }
    }

    private void doLoad() throws IOException {
        long size = randomFile.length();
        if (size == 0) {
            randomFile.setLength(8L);
            randomFile.seek(0);
            randomFile.writeInt(0); // term
            randomFile.writeInt(0); // votedFor length
        } else {
            term = randomFile.readInt();
            int length = randomFile.readInt();
            if (length > 0) {
                byte[] bytes = new byte[length];
                randomFile.read(bytes);
                NodeName tmp = serialize.deserialize(bytes, NodeName.class);
                votedFor = tmp;
            }
        }
    }

    @Override
    public int getTerm() {
        final ReentrantReadWriteLock.ReadLock read = readLock;
        read.lock();
        try {
            return term;
        } finally {
            read.unlock();
        }
    }

    @Override
    public boolean setTerm(int term) {
        final ReentrantReadWriteLock.WriteLock write = writeLock;
        write.lock();
        try {
            randomFile.seek(OFFSET_TERM);
            randomFile.writeInt(term);
            this.term = term;
            return true;
        } catch (IOException e) {
            return false;
        } finally {
            write.unlock();
        }
    }

    @Override
    public NodeName getVotedFor() {
        final ReentrantReadWriteLock.ReadLock read = readLock;
        read.lock();
        try {
            return votedFor;
        } finally {
            read.unlock();
        }
    }

    @Override
    public NodeName setVotedFor(NodeName votedFor) {
        final ReentrantReadWriteLock.WriteLock write = writeLock;
        write.lock();
        try {
            randomFile.seek(OFFSET_VOTED_FOR);
            if (votedFor == null) {
                randomFile.writeInt(0);
                randomFile.setLength(8L);
            } else {
                byte[] bytes = serialize.serialize(votedFor);
                randomFile.writeInt(bytes.length);
                randomFile.write(bytes);
            }
            this.votedFor = votedFor;
            return votedFor;
        } catch (IOException e) {
            return null;
        } finally {
            write.unlock();
        }
    }

    @Override
    public void close() throws IOException {
        final ReentrantReadWriteLock.WriteLock write = writeLock;
        write.lock();
        try {
            if (open) {
                doClose();
            }
        } finally {
            write.unlock();
        }
    }

    private void doClose() throws IOException {
        try {

        } finally {
            closeChannel();
        }
    }

    private void closeChannel() throws IOException {
        try {

        } finally {
            if (null != randomFile) {
                randomFile.close();
            }
        }
    }


    @Override
    public String toString() {
        return "RaftNodeStore{" +
                "dir=" + dir +
                ", file=" + file +
                ", local=" + local +
                ", term=" + term +
                ", votedFor=" + votedFor +
                '}';
    }

}
